<?php
/**
 *
 * Created by PhpStorm.
 * User: wuciyou
 * Email: 898060380@qq.com
 * Date: 08/03/2018
 * Time: 09:35
 */
namespace yagou;

use yagou\aop\YaGouRequest;

class YgClient {
    // 第三方应用ID
    public $appId;

    //网关
    public $gatewayUrl = "https://openapi.yggx.com/gateway";
    //返回数据格式
    public $format = "json";
    //api版本
    public $apiVersion = "1.0";
    // 表单提交字符集编码
    public $postCharset = "utf-8";

    /**
     * 第三方私钥文件
     * $rsaPrivateKeyPemFilePath 和 $rsaPrivateKeyPem  二选一传入
     */
    public $rsaPrivateKeyPemFilePath;
    public $rsaPrivateKeyPem;


    /**
     * 雅购开放平台公钥
     * $yagouPublicKeyPemPath 和 $yagouPublicKeyPem 二选一传入
     */
    public $yagouPublicKeyPemPath;
    public $yagouPublicKeyPem;

    // 是否开启调试信息
    public $debugInfo = false;

    // 调试信息日志保存目录
    public $logPath = './';

    private $fileCharset = "utf-8";

    //签名类型
    public $signType = "RSA2";

    protected $yagouSdkVersion = "yagou-sdk-php-20180308";

    public function generateSign($params, $signType = "RSA2") {
        $signContent = $this->getSignContent($params);
        $this->echoDebug('signContent:' . $signContent);
        return $this->sign($signContent, $signType);
    }

    protected function getSignContent($params) {
        ksort($params);

        $stringToBeSigned = "";
        foreach ($params as $k => $v) {
            if (false === $this->checkEmpty($v)) {

                // 转换成目标字符集
                $v = $this->characet($v, $this->postCharset);
                $stringToBeSigned .= $k . "=" . $v.'&';
            }
        }
        $stringToBeSigned = substr($stringToBeSigned, 0, -1);
        unset ($k, $v);
        return $stringToBeSigned;
    }

    protected function sign($data, $signType = "RSA2") {
        if($this->checkEmpty($this->rsaPrivateKeyPemFilePath)){
            $priKey=$this->rsaPrivateKeyPem;
            $res = "-----BEGIN RSA PRIVATE KEY-----\n" .
                wordwrap($priKey, 64, "\n", true) .
                "\n-----END RSA PRIVATE KEY-----";
        }else {
            $priKey = file_get_contents($this->rsaPrivateKeyPemFilePath);
            $res = openssl_get_privatekey($priKey);
        }

        ($res) or die('您使用的私钥格式错误，请检查RSA私钥配置');

        if ("RSA2" == $signType) {
            openssl_sign($data, $sign, $res, OPENSSL_ALGO_SHA256);
        } else {
            openssl_sign($data, $sign, $res);
        }

        if(!$this->checkEmpty($this->rsaPrivateKeyPemFilePath)){
            openssl_free_key($res);
        }
        $sign = base64_encode($sign);

        $this->echoDebug('sign:' . $sign);

        return $sign;
    }


    protected function curl($url, $postFields = null) {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $url);
        curl_setopt($ch, CURLOPT_FAILONERROR, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);

        $postBodyString = "";
        if (is_array($postFields) && 0 < count($postFields)) {

            foreach ($postFields as $k => $v) {
                $postBodyString .= "$k=" . urlencode($this->characet($v, $this->postCharset)) . "&";
            }
            unset ($k, $v);
            curl_setopt($ch, CURLOPT_POST, true);
            curl_setopt($ch, CURLOPT_POSTFIELDS, substr($postBodyString, 0, -1));
            $this->echoDebug('postBodyString:' . $postBodyString);
        }

        $headers = array('content-type: application/x-www-form-urlencoded;charset=' . $this->postCharset);
        curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);

        $reponse = curl_exec($ch);

        if (curl_errno($ch)) {

            throw new \Exception(curl_error($ch), 0);
        } else {
            $httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            if (200 !== $httpStatusCode) {
                throw new \Exception($reponse, $httpStatusCode);
            }
        }
        $this->echoDebug('reponse:' . $reponse);
        curl_close($ch);
        return $reponse;
    }

    protected function logCommunicationError($apiName, $requestUrl, $errorCode, $responseTxt) {
        $localIp = isset ($_SERVER["SERVER_ADDR"]) ? $_SERVER["SERVER_ADDR"] : "CLI";
        $logger = new Logger;
        $logger->conf["log_file"] = $this->logPath. '/' . "logs/aop_comm_err_" . $this->appId . "_" . date("Y-m-d") . ".log";
        $logger->conf["separator"] = "^_^";
        $logData = array(
            date("Y-m-d H:i:s"),
            $apiName,
            $this->appId,
            $localIp,
            PHP_OS,
            $this->apiVersion,
            $requestUrl,
            $errorCode,
            str_replace("\n", "", $responseTxt)
        );
        $logger->log($logData);
    }

    public function execute(YaGouRequest $request) {

        $this->setupCharsets($request);

        //  如果两者编码不一致，会出现签名验签或者乱码
        if (strcasecmp($this->fileCharset, $this->postCharset)) {

            // writeLog("本地文件字符集编码与表单提交编码不一致，请务必设置成一样，属性名分别为postCharset!");
            throw new Exception("文件编码：[" . $this->fileCharset . "] 与表单提交编码：[" . $this->postCharset . "]两者不一致!");
        }

        $iv = null;

        if (!$this->checkEmpty($request->getApiVersion())) {
            $iv = $request->getApiVersion();
        } else {
            $iv = $this->apiVersion;
        }

        //组装系统参数
        $sysParams["app_id"] = $this->appId;
        $sysParams["version"] = $iv;
        $sysParams["format"] = $this->format;
        $sysParams["sign_type"] = $this->signType;
        $sysParams["method"] = $request->getApiMethodName();
        $sysParams["timestamp"] = date("Y-m-d H:i:s");
        $sysParams["yagou_sdk"] = $this->yagouSdkVersion;
        $sysParams["charset"] = $this->postCharset;

        //获取业务参数
        $apiParams['biz_content'] = $request->getApiParas();

        if(!isset($apiParams['biz_content'])){
            throw new Exception(" api request Fail! The reason : encrypt request is not supperted!");
        }

        $apiParams['biz_content'] = $this->rsaEncrypt($apiParams['biz_content']);

        //签名
        $sysParams["sign"] = $this->generateSign(array_merge($apiParams, $sysParams), $this->signType);

        //系统参数放入GET请求串
        $requestUrl = $this->gatewayUrl . "?";
        foreach ($sysParams as $sysParamKey => $sysParamValue) {
            $requestUrl .= "$sysParamKey=" . urlencode($this->characet($sysParamValue, $this->postCharset)) . "&";
        }
        $requestUrl = substr($requestUrl, 0, -1);
        $this->echoDebug('requestUrl:' . $requestUrl);
        //发起HTTP请求
        try {
            $resp = $this->curl($requestUrl, $apiParams);
        } catch (Exception $e) {

            $this->logCommunicationError($sysParams["method"], $requestUrl, "HTTP_ERROR_" . $e->getCode(), $e->getMessage());
            return false;
        }

        //解析AOP返回结果
        $respWellFormed = false;


        // 将返回结果转换本地文件编码
        $r = iconv($this->postCharset, $this->fileCharset . "//IGNORE", $resp);

        $signData = null;
        $respObject = null;

        if ("json" == $this->format) {

            $respObject = json_decode($r,true);
            if (null !== $respObject) {
                $respWellFormed = true;
            }
        }

        //返回的HTTP文本不是标准JSON，记下错误日志
        if (false === $respWellFormed) {
            $this->logCommunicationError($sysParams["method"], $requestUrl, "HTTP_RESPONSE_NOT_WELL_FORMED", $resp);
            return false;
        }

        if($respObject['code'] != 'SUCCESS'){
            return $respObject;
        }

        // 验签
        if($this->rsaCheckV2($respObject) && isset($respObject['biz_content'])){
            // 解密
            if(isset($respObject['encrypt']) && $respObject['encrypt'] === true){
                $bizContentStr = $this->rsaDecrypt($respObject['biz_content']);
            }else{
                $bizContentStr = $respObject['biz_content'];
            }

            if(isset($respObject['type'])){
                switch ($respObject['type']){
                    case "EMPTY":
                        return '';
                    case 'ARRAY':
                        $respObject['biz_content'] =  json_decode($bizContentStr,true);
                        break;
                    case 'STRING':
                        $respObject['biz_content'] = $bizContentStr;
                        break;
                }
            }

        }else{
            $this->logCommunicationError($sysParams["method"], $requestUrl, "RESPONSE_RSA_CHECK_FAIL", $resp);
        }
        unset($respObject['encrypt']);
        unset($respObject['type']);
        unset($respObject['sign']);

        return $respObject;
    }


    /**
     * 获取异步回调的数据
     * @param null $body
     * @return array
     * @throws \Exception
     */
     function getCallBackData($body = null){
         if($body == null){
            $body = file_get_contents("php://input");
         }
         $bodyJson = json_decode($body,true);

         // 验签
         if($this->rsaCheckV2($bodyJson) && isset($bodyJson['biz_content'])) {
             // 解密
             if (isset($bodyJson['encrypt']) && $bodyJson['encrypt'] === true) {
                 $bizContentStr = $this->rsaDecrypt($bodyJson['biz_content']);
             } else {
                 $bizContentStr = $bodyJson['biz_content'];
             }
             $bizContent = '';
             if (isset($bodyJson['type'])) {
                 switch ($bodyJson['type']) {
                     case "EMPTY":
                         $bizContent = '';
                         break;
                     case 'ARRAY':
                         $bizContent = json_decode($bizContentStr, true);
                         break;
                     case 'STRING':
                         $bizContent = $bizContentStr;
                         break;
                 }
             }

             // 返回解密后的数据
             return array(
                 'event'        => $bodyJson['event'],
                 'biz_content'=> $bizContent,
             );
         }else{
             throw new \Exception("验签失败");
         }
     }

    /**
     * 转换字符集编码
     * @param $data
     * @param $targetCharset
     * @return string
     */
    function characet($data, $targetCharset) {

        if (!empty($data)) {
            $fileType = $this->fileCharset;
            if (strcasecmp($fileType, $targetCharset) != 0) {
                $data = mb_convert_encoding($data, $targetCharset, $fileType);
                //				$data = iconv($fileType, $targetCharset.'//IGNORE', $data);
            }
        }

        return $data;
    }

    /**
     * 校验$value是否非空
     *  if not set ,return true;
     *    if is null , return true;
     **/
    protected function checkEmpty($value) {
        if (!isset($value))
            return true;
        if ($value === null)
            return true;
        if (trim($value) === "")
            return true;

        return false;
    }

    /** rsaCheckV2
     *  验证签名
     *  在使用本方法前，必须初始化AopClient且传入公钥参数。
     *  公钥是否是读取字符串还是读取文件，是根据初始化传入的值判断的。
     **/
    public function rsaCheckV2($params, $signType='RSA2') {
        $sign = $params['sign'];
        $params['sign'] = null;
        return $this->verify($this->getSignContent($params), $sign, $signType);
    }

    function verify($data, $sign, $signType = 'RSA2') {

        if($this->checkEmpty($this->yagouPublicKeyPemPath)){

            $pubKey= $this->yagouPublicKeyPem;
            $res = "-----BEGIN PUBLIC KEY-----\n" .
                wordwrap($pubKey, 64, "\n", true) .
                "\n-----END PUBLIC KEY-----";
        }else {
            //读取公钥文件
            $pubKey = file_get_contents($this->yagouPublicKeyPemPath);
            //转换为openssl格式密钥
            $res = openssl_get_publickey($pubKey);
        }

        ($res) or die('RSA公钥错误。请检查公钥文件格式是否正确');

        //调用openssl内置方法验签，返回bool值

        if ("RSA2" == $signType) {
            $result = (bool)openssl_verify($data, base64_decode($sign), $res, OPENSSL_ALGO_SHA256);
        } else {
            $result = (bool)openssl_verify($data, base64_decode($sign), $res);
        }

        if(!$this->checkEmpty($this->yagouPublicKeyPemPath)) {
            //释放资源
            openssl_free_key($res);
        }

        return $result;
    }


    public function rsaEncrypt($data, $charset="utf-8") {

        if($this->checkEmpty($this->yagouPublicKeyPemPath)){

            $pubKey= $this->yagouPublicKeyPem;
            $res = "-----BEGIN PUBLIC KEY-----\n" .
                wordwrap($pubKey, 64, "\n", true) .
                "\n-----END PUBLIC KEY-----";
        }else {
            //读取公钥文件
            $pubKey = file_get_contents($this->yagouPublicKeyPemPath);
            //转换为openssl格式密钥
            $res = openssl_get_publickey($pubKey);
        }

        $blocks = $this->splitCN($data, 0, 30, $charset);
        $chrtext  = null;
        $encodes  = array();
        foreach ($blocks as $n => $block) {
            if (!openssl_public_encrypt($block, $chrtext , $res)) {
                echo "<br/>" . openssl_error_string() . "<br/>";
            }
            $encodes[] = urlencode(base64_encode( $chrtext ));
        }
        $chrtext = implode(",", $encodes);

        if(!$this->checkEmpty($this->yagouPublicKeyPemPath)) {
            //释放资源
            openssl_free_key($res);
        }

        return $chrtext;
    }

    public function rsaDecrypt($data) {

        if($this->checkEmpty($this->rsaPrivateKeyPemFilePath)){

            $pubKey= $this->rsaPrivateKeyPem;
            $res = "-----BEGIN PUBLIC KEY-----\n" .
                wordwrap($pubKey, 64, "\n", true) .
                "\n-----END PUBLIC KEY-----";

        }else{
            //读取私钥文件
            $priKey = file_get_contents($this->rsaPrivateKeyPemFilePath);
            //转换为openssl格式密钥
            $res = openssl_get_privatekey($priKey);
        }

        $decodes = explode(',', $data);
        $content = "";
        $dcyCont = "";
        foreach ($decodes as $n => $decode) {
            if (!openssl_private_decrypt(base64_decode(urldecode($decode)), $dcyCont, $res)) {
                echo "<br/>" . openssl_error_string() . "<br/>";
            }
            $content .= $dcyCont;
        }

        if(!$this->checkEmpty($this->rsaPrivateKeyPemFilePath)) {
            //释放资源
            openssl_free_key($res);
        }

        return $content;
    }

    function splitCN($cont, $n = 0, $subnum, $charset) {

        $arrr = array();
        for ($i = $n; $i < strlen($cont); $i += $subnum) {
            $res = $this->subCNchar($cont, $i, $subnum, $charset);
            if (!empty ($res)) {
                $arrr[] = $res;
            }
        }

        return $arrr;
    }

    function subCNchar($str, $start = 0, $length, $charset = "utf-8") {
        if (strlen($str) <= $length) {
            return $str;
        }
        $re['utf-8'] = "/[\x01-\x7f]|[\xc2-\xdf][\x80-\xbf]|[\xe0-\xef][\x80-\xbf]{2}|[\xf0-\xff][\x80-\xbf]{3}/";
        $re['gb2312'] = "/[\x01-\x7f]|[\xb0-\xf7][\xa0-\xfe]/";
        $re['gbk'] = "/[\x01-\x7f]|[\x81-\xfe][\x40-\xfe]/";
        $re['big5'] = "/[\x01-\x7f]|[\x81-\xfe]([\x40-\x7e]|\xa1-\xfe])/";
        preg_match_all($re[$charset], $str, $match);
        $slice = join("", array_slice($match[0], $start, $length));
        return $slice;
    }

    private function setupCharsets($request) {
        if ($this->checkEmpty($this->postCharset)) {
            $this->postCharset = 'utf-8';
        }
        $str = preg_match('/[\x80-\xff]/', $this->appId) ? $this->appId : print_r($request, true);
        $this->fileCharset = mb_detect_encoding($str, "UTF-8, GBK") == 'UTF-8' ? 'UTF-8' : 'GBK';
    }

    function echoDebug($content) {

        if ($this->debugInfo) {
            $localIp = isset ($_SERVER["SERVER_ADDR"]) ? $_SERVER["SERVER_ADDR"] : "CLI";
            $logger = new Logger;
            $logger->conf["log_file"] = $this->logPath. '/' . "logs/aop_comm_debug_" . $this->appId . "_" . date("Y-m-d") . ".log";
            $logger->conf["separator"] = "^_^";
            $logData = array(
                date("Y-m-d H:i:s"),
                $this->appId,
                $localIp,
                PHP_OS,
                $this->apiVersion,
                $content
            );
            $logger->log($logData);
        }

    }

}